• (unregistered)

    Um, let's look at the code:

    static int GetTableRowCount(string tableName)
    {
    int count = 0;
    SqlCommand cmd = new SqlCommand();
    cmd.Connection = getConn();
    cmd.CommandText = "SELECT * FROM [" + tableName + "]";
    SqlDataReader dr = cmd.ExecuteReader();
    while (dr.Read()) { count += 1; }
    return count;
    }

    And now let's look at your comment:

        Seeing as this is C#, not C++, you're wrong on all counts.

    How can you possibly state categorically that this is C# code, from the information provided? You can't, because SqlCommand can easily be a class, and the rest is easily C++/STL code. So, you're not exactly "right on all counts" yourself.

  • (cs) in reply to
    :
    Um, let's look at the code:
    static int GetTableRowCount(string tableName)
    {
    int count = 0;
    SqlCommand cmd = new SqlCommand();
    cmd.Connection = getConn();
    cmd.CommandText = "SELECT * FROM [" + tableName + "]";
    SqlDataReader dr = cmd.ExecuteReader();
    while (dr.Read()) { count += 1; }
    return count;
    }

    And now let's look at your comment:

        Seeing as this is C#, not C++, you're wrong on all counts.

    How can you possibly state categorically that this is C# code, from the information provided? You can't, because SqlCommand can easily be a class, and the rest is easily C++/STL code. So, you're not exactly "right on all counts" yourself.

    What you don't get is the WTF has nothing to do with the specific piece of code -- it is an EXAMPLE!  did you read the original post? Did you read the comments where we also reiterated this fact a few times? 

    Nothing is sadder than people who attempt to demonstrate their superior intelligence when they have no clue what is actually going on ... (though it can be entertaining sometimes, I guess)

    Also -- try logging on or at least signing your posts so we can at least know which posts belong to who. That's the most frustrating thing about this site: "I am going to prove how smart I am to the world -- I just won't let them know who I am in case I happen to be wrong!"  Weak.

  • (cs) in reply to
    :

    How can you possibly state categorically that this is C# code, from the information provided?
    Of all the Agol family languages in common use, it must be either Java or C#.  Seeing as Java's string type is called "String" (not string), it is a very reasonable assumption that it is C#, seeing as it's not Java..

    You can't, because SqlCommand can easily be a class,
    And it's instantiation isn't valid C++.   If it were:[code language="c++"]SqlCommand *cmd = new SqlCommand();[/code] then you'd be correct.  But it's not, so it cannot be.  The other correct C++ instantiation would be:[code language="c++"]SqlCommand cmd;[/code]

  • (cs) in reply to LordHunter317
    LordHunter317:
    [image]  wrote:

    How can you possibly state categorically that this is C# code, from the information provided?
    Of all the Agol family languages in common use, it must be either Java or C#.  Seeing as Java's string type is called "String" (not string), it is a very reasonable assumption that it is C#, seeing as it's not Java..

    You can't, because SqlCommand can easily be a class,
    And it's instantiation isn't valid C++.   If it were:

    <font face="Lucida Console, Courier" size="2">SqlCommand *cmd = new SqlCommand();</font>
     
    then you'd be correct.  But it's not, so it cannot be.  The other correct C++ instantiation would be:

    <font face="Lucida Console, Courier" size="2">SqlCommand cmd;</font>
     


  • (cs) in reply to finix

    Thou shalt be careful with words such as "cannot" et al [;)]


    [code language="c#"]
    class SqlCommand
    {
    public:
    SqlCommand( const SqlCommand* );
    //...
    };
    [/code]

  • (unregistered) in reply to
    :
    No way

    You are definately joking.

    This is

    No

    No nono. This can't be.  You lie.


    I've met this kind of approach in some code I've inherited. Only in PHP.. something like

    [code language="php"]
    $rs = mysql_query("SELECT * FROM some_table");
    while ( ($rc = mysql_fetch_row($rs)) !== false ) { $count++; }
    [/code]

    I have the former developer suspected for not knowing exactly what a database is.. I've found this somewhat related code:

    [code language="php"]
    $rs = mysql_query("SELECT * FROM users ORDER BY username ASC");
    while ( ($rc = mysql_fetch_array($rs, MYSQL_ASSOC)) !== false )
    {
        extract($rc);
        if ( $username == $search ) { break; }
    }
    [/code]

    Sad, isn't it..?
  • (cs) in reply to finix

    Finix:
    Yes, but seeing as it would be leaking memory all over the place in the given context (in C++), it's not very interesting.

  • (unregistered)

    <font color="#0000ff">What you don't get is the WTF has nothing to do with the specific piece of code -- it is an EXAMPLE!  did you read the original post? Did you read the comments where we also reiterated this fact a few times? </font>

    Oh, I see. So because it's an example, I'm not allowed to point out the errors in it. Wow, excuse me. Surely the point of this IS to point out the errors in the example - after all, that's what everyone else has done with the SQL part of this snippet. And as this is an "EXAMPLE" (sic), how can you state it's C#? The argument you're using against my comment works against yours, too. There is nothing in the code to prove, definitively, what language it is.

    <font color="#0000ff">Also -- try logging on or at least signing your posts so we can at least know which posts belong to who. That's the most frustrating thing about this site: "I am going to prove how smart I am to the world -- I just won't let them know who I am in case I happen to be wrong!"  Weak.</font>

    Signing my posts so you can know who I am? What does that have to do with anything? You know the same person is responding. Anyway, thanks Jeff, now that you've posted with your name I know, uh, well, nothing about you. Irrelevant point, but feel free to continue attacking off-topic issues such as whether or not I'm logged in. It's easier than discussing the issue at hand.

    <font color="#0000ff"> Yes, but seeing as it would be leaking memory all over the place in the given context (in C++), it's not very interesting.</font>
    Badly written code that, oh, I don't know, leaks all over the place. Wow, someone should start a site that posts those pieces of code for people to chuckle at. They could look at it and respond, "WTF!". Someone could update the site each day with new things to laugh at. You could probably even call the site "thedailywtf.com", but looks like that FQDN is taken. Never mind.

    Also LordHunter, your previous comment about the code being C# due to the use of <font color="#0000ff">string </font>is not necessarily correct. As I said before, <font color="#0000ff">string </font>is also an STL type.

    I apologise for interrupting your clique with valid comments that you didn't take the time to fully consider.

  • (cs) in reply to

    <font face="Georgia">I propose a new rule: commenters may only throw idiot hissy fits if they're logged in.  Granted it could seriously reduce the number of comments, but I'm sure the benefits outweigh the costs.

    ObWTF: they should have done it in Scheme.  Using macros.  And completions.  With a suitable set of definitions, the entire original posting could be rewritten as:
    </font>

    <font face="Georgia"><font face="Courier New">(define (count-records table db)</font></font>
    <font face="Georgia"><font face="Courier New">  (die-in-screaming-agony ,@table #t #t '() ,,db))</font></font>
    <font face="Georgia">... which is obviously much easier to read!
    </font>
  • (cs) in reply to

    >>I apologise for interrupting your clique with valid comments that you didn't take the time to fully consider.

    Wow ... you simply just don't get it.   [:(]

    If someone asks you your name, you interpret that as an attempt to exclude you from their "clique" ?  Come on, grow up!

    Please stick around and contribute, and if you want to post multiple times and have an intelligent discussion, don't you think it would help if you somehow mark your posts so we can distinguish yours from the other anon posts?  Do you understand that?  Look at the anonymous posts in this thread -- how can you tell who is responding to who?  We don't want your friggin social security number, we don't need your real name.  

     

  • (cs)

    > There is nothing in the code to prove, definitively, what language it is.

    Perhaps not, but I find it hard to believe anyone familiar with both languages could really suggest that its not C#.  The use of names originating from the System.Data namespace alone is enough to convince me.

  • (cs) in reply to LordHunter317
    LordHunter317:
    Finix:
    Yes, but seeing as it would be leaking memory all over the place in the given context (in C++), it's not very interesting.


    Yes, you're right. Albeit it doesn't necessarily leak anything, it isn't very interesting. But, then again, so is the whole function in the given context.
    Just put that constructor there to prove your statements ("no valid C++", "cannot be") wrong.
  • (unregistered) in reply to

    I'm with that other anonymous guy. I don't like being harassed to "log in" with a frickin' screen name.

    What is this...big brother? Why do you want to track us so bad?

    I'm never logging in. And that's just to prove a point to Jeff S. and all you other forum militants.

  • (unregistered)
    And as this is an "EXAMPLE" (sic)


    What's with the (sic) ?!?!  Or do you not know what (sic) means?
  • (unregistered)

    <FONT style="BACKGROUND-COLOR: #efefef">This piece of code has the potential to do a lot more than just count.  </FONT>

    <FONT style="BACKGROUND-COLOR: #efefef">It's prime for a SQL injection attack.</FONT>

     

    <FONT style="BACKGROUND-COLOR: #efefef"></FONT> 

  • (cs) in reply to

    Yes, I see.

    Perhaps the original authors are way ahead of everybody else, and decided to put a few backdoors into these systems.

  • (cs)

    By the way... C# does have pointers. You'll have to use an unsafe block, or mark the whole assembly. It'll obviously need full permission to run too.

  • (cs) in reply to phx

    Oh, I see. So because it's an example, I'm not allowed to point out the errors in it.
    You're more than welcome to, but if you're wrong about the errors you call out, expect to be called on them yourself. In this case you were wrong, irrespective of language.

    And as this is an "EXAMPLE" (sic) [sic], how can you state it's C#?
    Aside: if you're going to edit a direct quote, please do so according the rules of grammar. I usually don't care much on an online forum, but misquoting of direct quotes and misediting as well tends to upset me. It's disrespectful to someone else's words.

    I showed my justifications as to why it must be C#. While I cannot conclusively say it's C#, of the languages in common use on this forum, it's most likely to be.

    Badly written code that, oh, I don't know, leaks all over the place.
    But, seeing as in C++, in order to copy an object from a pointer to an object, you must explictly make a constructor to do so, it's reasonable to assume the code author would have been aware of, and dealt with, the memory issue. You can't just copy construct from an object pointer in C++. Seeing as the construct shown is identical to the only way to initalize an object in C# and Java, and it's not a "normal" C++ construct, it's highly unlikely it's C++.

    Nevermind that even though you can do it in C++, it's a rather nonsensical and dangerous construct anyway, for the very reason that code points out.

    Also LordHunter, your previous comment about the code being C# due to the use of <font color="#0000ff">string </font>is not necessarily correct. As I said before, <font color="#0000ff">string </font>is also an STL type.
    You'll also note I never said it could not be C++ because of the use of the word "string".  I said it couldn't be Java.   I said it wasn't C++ due to the way the SqlCommand object was constructed.

    I apologise for interrupting your clique with valid comments that you didn't take the time to fully consider.
    And I apologies for interrupting your childish, anonymous, highly inaccurate and simply wrong tirade against the posters of this site.

    Fact: Your first post pointing out the errors in the code contained several errors, even if the code was C++.  Notably:
    Hello! It's a pointer to the heap! Let's try cmd->Connection, kiddies
    The object in the code isn't declared as a pointer in any language we've considered.  You falsly assumed based on the use of the "new" keyword and the fact the construct is very much like freestore allocation in C++, that the declared object must be a pointer.  The only way it could be is if "SqlCommand" were a typedef, but the code would fail to compile anyway, because it's allocation would fail.
    The fact of the matter is that the lines are perfectly valid C# and/or Java, as that's how you declare, allocate, initialze, and allocate objects in both languages.  As I pointed out, it's close to being valid C++, and it could be if and only if there is a constructor for "SqlCommand" that constructs from a pointer to a "SqlCommand" object.  And if that were true (and it was valid C++), the line you point out as wrong would still be valid anyway, because the object being used isn't a pointer at all, but a live stack-based object.  Meaning you use the dot operator, not the arrow.

    So, you're wrong on point number 1.

    I'll ignore the memory allocation issues since you're contesting the language so hotly [8-)]

    You're wrong on your last point about the meaning of 'static' w.r.t to a function.  In C and C++, for a non-member function, static affects the linkage of the function and makes it local to the file it was declared in.  In all relevant languages, a static member function (meaning the member of a class) means that it's data is shared between all instances of an object; it does not recieve a pointer/reference to the object it's being called from.  It also means it can be called without any instances of the object being alive.  It does not mean the function needs to worry about allocated resources or checking to see if they're NULL.  I'm not sure where you came up with that, but I think you were thinking of static local variables, which retain their value between calls to a function.

    I'd like to point out as an aside: you're quibiling about the language this code could be, but you can't even point out what's wrong with in one language, let alone more than one.  Leading me to wonder how you can possibly contest it's language.  It's not like the code's a trick question or anything.

    phx:
    By the way... C# does have pointers. You'll have to use an unsafe block, or mark the whole assembly. It'll obviously need full permission to run too.
    Yes, I know, but they're not frequently used, so I "omitted" them ;)
  • (cs)

    I think the best clue to this example being C# is that Alex wrote it. And if I remember the previous items on this forum correctly Alexprefers to use a .Net language.

    This doesn't prove anything, but I just wanted to point it out. (And I could be wrong about Alex).

    Drak

  • (unregistered) in reply to

    :
    I've seen this several times in the past with straight ASP/VBScript.  When I questioned it, I was told that some of the ODBC drivers in use did not provide a count back to the dataset so this was the work around. 
    shudder...

    Huh? You can tell them they are idiots.

    In ASP one typically uses ADODB.RecordSet, and that has a property called .RecordCount that (indeed ! Who'ld have thought?) has a count of the records. In some (!! exceptional ) circumstances, it may be necessary to move the cursor to the last record before the recordcount is available though. But moving to the last item is still not a scripted loop with a counter.

  • (unregistered)

    You know... you're all pretty lame. And what's the fun in "making up" WTF's like Alex did?

  • (cs)

    Here's another great forum falling apart because of the trolling... WTF, people, we were supposed to have fun together!

  • (cs) in reply to felix

    It's not the trolls themselves who spoil the fun on this forum. It's the people who respond to these trolls who are making things worse. Give a troll some attention and he will continue to troll around some more. Ignore the comments the trolls make and they soon get bored...

  • (unregistered)

     "SELECT * FROM [" + tableName + "]";

    Can you do string concatenation like that in C++ without overloading the + operator in some sort of weird fashion? I see the odds of this being C++ droping quite rapidly.

  • (cs) in reply to Drak

    Drak:
    I think the best clue to this example being C# is that Alex wrote it. And if I remember the previous items on this forum correctly Alexprefers to use a .Net language.

    One was in C#, another PHP, and the third in VB (either Script or 6). The PHP function had the table name as a param (like this: "select * from $tableName").

    You are correct, I do prefer .NET. But it really came down to the fact that I have a Syntax Hilighter that easily does C#. It is kinda fun though to see the "what language" discussion, though :-D.

    As for the trolls, when I roll out the new version of the software, anonymous posting will still be permitted; there is too small a percentage or readers/registeredUsers. But the posting will be more .Text style ... you enter your name and URL, and it remembers you via cookie. I think that will encourage better discussion ...

  • (cs) in reply to
    :

    I'm with that other anonymous guy. I don't like being harassed to "log in" with a frickin' screen name.

    What is this...big brother? Why do you want to track us so bad?

    I'm never logging in. And that's just to prove a point to Jeff S. and all you other forum militants.



    And you know why? You're a coward. You're afraid that if you post enough BS people will see your name, assume BS and ignore you. Or worse, tell you to STFU.
  • (unregistered) in reply to

    Typically we have always used SELECT COUNT(Id) FROM Table . Since Id Is the Primary Key and cannot be null..  After a bit of testing this is faster by a tiny bit. (1.193 vs 1.86 sec)  Your table does have a primary key right?

  • (unregistered) in reply to fregas

    fregas:
    <FONT size=3>That shouldn't be the issue.  As others have mentioned, a count of records in the table should happen through select count(*) on the database, not thru ADO or on the application server.  recordset.Count is handy, or in .NET: DataTable.Rows.Count, but even if you cannot use those options, a seperate query to database for the number of rows is immensely more efficient than looping thru the records.</FONT>

     

    I've seen it too, I think because count on it's own will not return a column name. The way around it was to:

    select count(*) as RecordCount from MyTable

     

  • (cs) in reply to

    :
    Typically we have always used SELECT COUNT(Id) FROM Table . Since Id Is the Primary Key and cannot be null..  After a bit of testing this is faster by a tiny bit. (1.193 vs 1.86 sec)  Your table does have a primary key right?

    That long to return a count? My goodness!! All RDMS I've worked with (even oracle) will do a full index scan on the PK index if * is selected.  What DB are you using?!?

  • (unregistered) in reply to

    <FONT color=#0000ff>"In ASP one typically uses ADODB.RecordSet, and that has a property called .RecordCount that (indeed ! Who'ld have thought?) has a count of the records. In some (!! exceptional ) circumstances, it may be necessary to move the cursor to the last record before the recordcount is available though. But moving to the last item is still not a scripted loop with a counter."</FONT>

    <FONT color=#000000>You're right, RecordCount is not a scripted loop, but it still may end up being a loop that pulls all the records to the client side to count them:</FONT>

    <FONT color=#006400>"If the Recordset object does not support approximate positioning, this property may be a significant drain on resources because all records will have to be retrieved and counted to return an accurate RecordCount value." </FONT><FONT color=#0000ff>http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ado270/htm/mdprorecordcount.asp</FONT>

    <FONT color=#000000>The default and most efficient cursor for most uses (adOpenForwardOnly) does not support approximate positioning. So if you do not RTFM and use RecordCount blindly you may be asking ADO to run the same silly WTF loop, but hidden from your view of course. It's not a problem if your result sets tend to be a dozen or so records that you are going to process anyway, but it's a massive cluster-WTF if all you wanted to do was count records.</FONT>

     

     

  • (unregistered)

    I have seen books recommend using COUNT(1) instead of COUNT(*) because of bugs in previous versions of Oracle and I have seen the difference in performance.

  • (cs)

    OH DEAR GOD!  WHAT A SERIOUS MEMORY LEAK!!  ...

    You're FIRED!

  • (cs) in reply to bat
    bat:
    <font face="Georgia">I propose a new rule: commenters may only throw idiot hissy fits if they're logged in.  Granted it could seriously reduce the number of comments, but I'm sure the benefits outweigh the costs.

    ObWTF: they should have done it in Scheme.  Using macros.  And completions.  With a suitable set of definitions, the entire original posting could be rewritten as:
    </font>
    <font face="Georgia"><font face="Courier New">(define (count-records table db)</font></font>
    <font face="Georgia"><font face="Courier New">  (die-in-screaming-agony ,@table #t #t '() ,,db))</font></font>
    <font face="Georgia">... which is obviously much easier to read!
    </font>


    you forgot the <font face="Courier New">(requires 'srfi-666) <font face="Times New Roman">part. [:P]</font></font>
  • (cs) in reply to
    Anonymous says "When I questioned it, I was told that some of the ODBC drivers in use did not provide a count back to the dataset so this was the work around."
     
    If the author was trying to get the record count directly from the recordset... I can, maybe, believe that with some combinations of software,if you open a recordset object (with Select blah blah blah) and then ask for its Count or Records property.... it might possibly not give the right number of records back.  (A Recordset.MoveLast might be required first, if I'm thinking of the right idiom.)  Or maybe there's a batchsize that's not being taken into consideration.  (Sometimes the count property is 1 when you first open the recordset.)
     
    But, as has been pointed out, using Select count(*) INSTEAD OF trying to count the records in the recordset will always give the right answer.  If it doesn't, then open a trouble ticket!
     
    David Walker

  • (cs) in reply to

    Hey that's MY line! [:P]

  • (unregistered) in reply to

    ow man, are you trying to get me killed ... [:'(]

  • (unregistered)

    This is C# and

    Table.Rows.Count would have taken care of this.  Enough said. 

  • (cs) in reply to
    :

    This is C# and

    Table.Rows.Count would have taken care of this.  Enough said. 

    Sarcasm?  A "devil's advocate" approach?  Honest opinion of the best approach?  Typical anonymous troll?

    Might be just me, but how can you tell?  I will assume, due to the lack of icons and/or excessive punctuation, that the poster is serious. 

    And, therefore, deserves a coveted "WTF Comment" award. And a response: 

    Did you read any of the comments?  The database layer should return 1 single value, 4 bytes, accross the network -- SELECT COUNT(*) --  and THAT'S IT.  To use Table.Rows.Count or something similiar, the DB must return ALL ROWS from the table to the application.  And that is exactly what the "WTF" of this example is doing, and that is WHY it is a "WTF". 

    Whether you loop manualy or use a method on the client that does the same, you should never do this.

  • (unregistered) in reply to

    <FONT size=2> </FONT>

    <FONT size=1>This is C# and </FONT>

    <FONT size=1>Table.Rows.Count would have taken care of this.  Enough said</FONT>

    <FONT size=1>This is a lesser WTF and really only excusable if you've already asked for the full dataset that you want to count for some other god forsaken reason, like filling a dropdown list or something, and have already fetched all the records locally.  At that point you may as well just get the record count by asking the memory object for its Rows.Count.  </FONT>

    <FONT size=1>In all other cases 1) you don't need all the data from every row locally or  2) you haven't bothered to fetch all the data across the network and don't  really expect to.  It is faster and easier to 'Select Count(*) as RowCount from <TableName>' (feel free to replace * with <PrimaryKey> if you like). Aliasing the column will work around the crappy drivers problem if that is a problem for you. </FONT>

  • (unregistered) in reply to

    From every test I have ever seen of this in Oracle, there is no difference in performance between Count(1) and Count(*). For a fairly detailed test case, check this link out: http://www.speakeasy.org/~jwilton/oracle/count-star.html

  • (unregistered) in reply to

    Select count(smallest_column) is equal but faster than select count(*), no?

    No. select count(*) allows the server to choose the most efficient method of getting the count from the specified table. Select count( blah ) might force the server to use a column that has no index and probably end up running something similar to the WTF code that started this travesty.

  • (unregistered) in reply to

    Yeah, nice, and then you still have to stream all that junk across the network to your client. What good did it do you to utilize the cache, again?  :)

  • Peter Praphon (unregistered) in reply to

    [:$] de que hablan ? no es necesario hacer ningun procedimiento para contar los registros si existe la funcion count

    Select *, count(id) as num from table1;  [H]

    Anonymous:
    Yeah, nice, and then you still have to stream all that junk across the network to your client. What good did it do you to utilize the cache, again?  :)

  • Peter Praphon (unregistered) in reply to Peter Praphon

    Sorry [:$]

    Correect...

    Select *, count(id) as num from table1 group by name; 

  • The princess (unregistered)

    That's not nice that you made fun of whoever wrote the loop thing but you never said what's the right way of doing it!!

    So...

    myCmd.CommandText = "SELECT COUNT(*) FROM the_table WHERE id = 5"
    myReader = myCmd.ExecuteReader()

    Dim amount As Integer
    If myReader.Read() Then
                amount = myReader.GetValue(0)
    End If

  • 1 (unregistered) in reply to fregas
    fregas:

    <FONT size=3>[I've seen this several times in the past with straight ASP/VBScript.  When I questioned it, I was told that some of the ODBC drivers in use did not provide a count back to the dataset so this was the work around. 
    shudder...]</FONT>

    <FONT size=3>That shouldn't be the issue.  As others have mentioned, a count of records in the table should happen through select count(*) on the database, not thru ADO or on the application server.  recordset.Count is handy, or in .NET: DataTable.Rows.Count, but even if you cannot use those options, a seperate query to database for the number of rows is immensely more efficient than looping thru the records.</FONT>

    <FONT size=3>LAME!</FONT>

  • (cs)
    Alex Papadimoulis:

    I've been hanging on to this snippet of code (from Brett J.) for quite a while now, mostly because I didn't think it was real. I mean, really, no professional programmer would ever retrieve an entire table and loop through each row, just to get a row count ... right?

     

    I refuse to be slandered.  I do not write code like that, I write right code.  But Jeff would disagree because he always says I should write LEFT code.

     

    [:P]

     

    He look, a martini...[D]wish Bill would put one of those up...

  • (cs) in reply to

    Anonymous:
    You know... you're all pretty lame.

    You're still here?  Having trouble finding other websites to browse?

  • (cs) in reply to
    Anonymous:
    I'm guessing many of you here have little or no experience developing enterprise software.  An important goal in developing enterprise software is scalability and in order to scale, often times you want to distribute the load away from your SQL server to a middle-tier server.  By counting the records on the middle-tier you are saving valuable processor cycles on the database server.


    Brilliant!  Of course you're right:  returning the ENTIRE table to the middle-tier is much faster than asking the db for a count...




  • easyCoder (unregistered) in reply to Jeff S

    Get a brain moran !

    count(*) multiplies the count by the number of columns !


Leave a comment on “Count Rowula”

Log In or post as a guest

Replying to comment #:

« Return to Article